#!/usr/bin/python
import os
from utils import _exec_system, _exec_shell1, Exp, _lock_file1, _unlock_file1, _derror, _dmsg, _set_value, _get_value
import errno
import time
import sys

class BcacheManage(object):
    def __init__(self, node):
        self.node = node
        self.config = self.node.config
        self.bcache_conf = '/etc/bcache/bcache.conf'
        if not os.path.exists(self.bcache_conf):
            os.system('mkdir -p /etc/bcache')
            os.system('touch %s' % (self.bcache_conf))
        self.cache_log = '%s/cache.log' % (self.config.log_path)
        if not os.path.exists(self.cache_log):
            _exec_system('mkdir -p %s' % self.config.log_path)
            os.system("touch %s" % (self.cache_log))

    def wipe_dev(self, dev):
        cmd = 'wipefs -a %s -f' % dev
        try:
            _exec_shell1(cmd, p=True)
        except Exp as e:
            raise Exp(errno.EPERM, "wipefs -a failed\n")
        return True

    def create_cachedev(self, cachedev):
        self.wipe_dev(cachedev)
        cmd = 'make-bcache -C %s --wipe-bcache' % cachedev
        _exec_shell1(cmd, p=True)

    def create_coredev(self, coredev):
        devname = coredev.split('/')[2]
        check_path = '/sys/block/%s/bcache' % devname

        self._stop_coredev_bcache_service(coredev)
        self.wipe_dev(coredev)
        cmd = 'make-bcache -B %s --wipe-bcache' % coredev
        _exec_shell1(cmd, p=True)

        retry = 0
        while True:
            if not os.path.exists(check_path):
                time.sleep(1)
                self.register_coredev(coredev)
                if retry > 3:
                     raise Exp(errno.EPERM, "create core dev %s fail !" % (coredev))
                retry += 1
            else:
                return

    def get_softdisk_by_dev(self, dev):
        disk_prefix = '/opt/fusionstack/data/disk/disk/'
        list_disk = os.listdir(disk_prefix)

        for softdisk in list_disk:
            abs_ln = os.path.join(disk_prefix, softdisk)
            target = os.readlink(abs_ln)
            if target == dev:
                return softdisk
        return None

    def remove_ln(self, coredev):
        mappingdev = self.get_mappingdev_by_coredev(coredev)
        if mappingdev is not None:
            diskprefix = '/opt/fusionstack/data/disk/disk'
            softdisk = self.get_softdisk_by_dev(mappingdev)
            if softdisk is not None:
                abs_softdisk = os.path.join(diskprefix, softdisk)
                _exec_rm = 'rm -f %s' % abs_softdisk
                _exec_shell1(_exec_rm, p=False)

    def register_coredev(self, coredev):
        register_path = '/sys/fs/bcache/register'

        if not os.path.exists(register_path):
            raise Exp(errno.EPERM, "bcache load error\n")

        _exec_register = 'echo %s > /sys/fs/bcache/register' % coredev
        try:
            _exec_shell1(_exec_register, p=True)
        except Exp as e:
            pass

    def bind_cache(self, cachedev, coredev, cache_policy, force):
        if not self.is_cachedev(cachedev):
            self.create_cachedev(cachedev)

        if self.is_coredev(coredev):
            if force:
                if self.is_attached_to_cache(coredev, cachedev):
                    _dmsg("%s already attached to cache device : %s, just ignore it !" % (coredev, cachedev))
                    return
                else:
                    _dmsg("dev:%s is a core device, will force destroy it!" % coredev)
                    self.remove_ln(coredev)
                    cset_uuid = self.get_cset_uuid_by_dev(coredev)
                    self.__del_coredev(coredev, cset_uuid)
            else:
                raise Exp(errno.EPERM, "dev:%s is already a core device, please check it" % (coredev))

        self.create_coredev(coredev)
        self._add_coredev_to_cache(coredev, cachedev)
        self.set_cache_policy(coredev, cache_policy)

    def is_coredev(self, dev):
        first_step = False
        devname = dev.split('/')[2]

        cmd = "bcache-super-show %s 2>/dev/null| grep 'backing device'" % dev
        check_path = '/sys/block/%s/bcache' % devname
        try:
            out, err = _exec_shell1(cmd, p=False)
            first_step = True
        except Exp as e:
            return False

        if first_step is True:
            if os.path.exists(check_path):
                return True
            else:
                return False
        else:
            return False

    def is_cachedev(self, dev):
        cmd = "bcache-super-show %s 2>/dev/null | grep 'cache device'" % dev
        try:
            out, err = _exec_shell1(cmd, p=False)
            return True
        except Exp as e:
            return False

    def _is_deleting(self, coredev):
        cmd = "grep 'delcoredev %s' %s" % (coredev, self.cache_log)
        try:
            _exec_shell1(cmd, p=False)
            return True
        except Exp, e:
            return False

    def get_status_by_coredev(self, coredev):
        coredevname = coredev.split('/')[2]

        if self._is_deleting(coredev):
            status = "deleting_cache"
        else:
            _exec_status = 'cat /sys/block/%s/bcache/running' % coredevname
            try:
                out, err = _exec_shell1(_exec_status, p=False)
            except Exp as e:
                #  _derror('dev:%s, ret: %d' % (coredev, e.errno))
                return None
            state = int(out.strip())
            if state == 1:
                status = 'running'
            else:
                status = 'stopped'

        return status

    def get_cachemode_by_coredev(self, coredev):
        coredevname = coredev.split('/')[2]

        _exec_mode = 'cat /sys/block/%s/bcache/cache_mode' % coredevname
        try:
            out, err = _exec_shell1(_exec_mode, p=False)
        except Exp as e:
            #  _derror('dev:%s, ret: %d' % (coredev, e.errno))
            return None
        list_mode = out.strip().split(' ')
        for mode in list_mode:
            if mode.startswith('['):
                return mode.split(']')[0].split('[')[1]

        return None


    def get_cacheinfo_by_coredev(self, coredev):
        if not self.is_coredev(coredev):
            return None, None, None, None

        cset_uuid = self.get_cset_uuid_by_dev(coredev)
        status = self.get_status_by_coredev(coredev)
        cachemode = self.get_cachemode_by_coredev(coredev)
        cachedev = self.get_cachedev_by_coredev(coredev)

        return cset_uuid, cachedev, status, cachemode

    def get_cacheinfo_by_cachedev(self, cachedev):
        if not self.is_cachedev(cachedev):
            return None, None, None

        cset_uuid = self.get_cset_uuid_by_dev(cachedev)

        return cset_uuid, None, None

    def _stop_coredev_bcache_service(self, coredev):
        devname = coredev.split('/')[2]
        check_path = "/sys/block/%s/bcache" % (devname)
        if os.path.isdir(check_path):
            cmd = "echo 1 > %s/stop" % (check_path)
            _exec_shell1(cmd, p=True)

    def get_mappingdev_by_coredev(self, coredev):
        if not self.is_coredev(coredev):
            return None

        cmd = 'lsblk %s --raw -o NAME|grep -v NAME' % coredev
        try:
            out, err = _exec_shell1(cmd, p=False)
        except Exp as e:
            raise Exp(errno.EPERM, 'get bcachename failed\n')
        list_dev = out.strip().split('\n')

        if len(list_dev) != 2:
            self._stop_coredev_bcache_service(coredev)
            raise Exp(errno.EAGAIN, '%s check lsblk fail(list_dev:%s), need retry\n' % (coredev, list_dev))

        if coredev == '/dev/' + list_dev[0]:
            return '/dev/' + list_dev[1]
        return None

    def get_coredev_by_fastdev(self, fastdev):
        bcachename = fastdev.split('/')[2]
        abs_ln = '/sys/block/%s/bcache' % bcachename
        if os.path.islink(abs_ln):
            target = os.readlink(abs_ln)
            coredev = target.split('/')[-2]
            return '/dev/' + coredev
        else:
            return None

    def get_cachedev_by_coredev(self, coredev):
        cset_uuid = self.get_cset_uuid_by_dev(coredev)
        cachedir = os.path.join('/sys/fs/bcache', cset_uuid)
        if os.path.exists(cachedir) and os.path.isdir(cachedir):
            files = os.listdir(cachedir)
            key = 'cache'
            for f in files:
                if key in f:
                    abs_ln = os.path.join(cachedir, f)
                    if os.path.islink(abs_ln):
                        target = os.readlink(abs_ln)
                        cachedev = target.split('/')[-2]
                        return '/dev/' + cachedev
        return None

    def get_cset_uuid_by_dev(self, dev):
        _exec = "bcache-super-show %s | grep cset.uuid | awk '{print $2}'" % dev
        try:
            out, err = _exec_shell1(_exec, p=False)
        except Exp as e:
            raise Exp(errno.EPERM, 'cant get coredev uuid\n')
        return out.strip()

    def is_attached_to_cache(self, coredev, cachedev):
        core_cset_uuid = self.get_cset_uuid_by_dev(coredev)
        cache_cset_uuid = self.get_cset_uuid_by_dev(cachedev)
        if core_cset_uuid != cache_cset_uuid:
            return False
        else:
            return True

    def _add_coredev_to_cache(self, coredev, cachedev):
        if not self.is_coredev(coredev):
            raise Exp(errno.EINVAL, '%s not a coredev\n' % (coredev))

        coredevname = coredev.split('/')[2]
        cache_set_uuid = self.get_cset_uuid_by_dev(cachedev)
        set_path = '/sys/block/%s/bcache/attach' % coredevname
        try:
            cmd = 'echo %s > %s' % (cache_set_uuid, set_path)
            _exec_shell1(cmd, p=True)
        except Exp, e:
            if self.is_attached_to_cache(coredev, cachedev):
                _dmsg("%s already attached to cache device : %s, just ignore it !" % (coredev, cachedev))
                pass
            else:
                raise Exp(errno.EPERM, "add coredev:%s to %s fail, %s" % (coredev, cachedev, e.err))

    def set_cache_policy(self, coredev, cache_policy):
        if not self.is_coredev(coredev):
            raise Exp(errno.EINVAL, '%s not a coredev\n' % (coredev))

        self.set_cache_mode(coredev, cache_policy['cache_mode'])
        self.set_cache_seq_cutoff(coredev, cache_policy['sequential_cutoff'])
        self.set_cache_wb_percent(coredev, cache_policy['writeback_percent'])

    def set_cache_wb_percent(self, coredev, wb_percent=None):
        if wb_percent is None or len(wb_percent) == 0:
            wb_percent = self.config.cache_wb_percent

        coredevname = coredev.split('/')[2]
        set_path = '/sys/block/%s/bcache/writeback_percent' % coredevname
        cmd = 'echo %s > %s' % (wb_percent, set_path)
        _exec_shell1(cmd, p=True)

    def set_cache_seq_cutoff(self, coredev, sequential_cutoff=None):
        if sequential_cutoff is None or len(sequential_cutoff) == 0:
            sequential_cutoff = self.config.cache_seq_cutoff

        coredevname = coredev.split('/')[2]
        set_path = '/sys/block/%s/bcache/sequential_cutoff' % coredevname
        cmd = 'echo %s > %s' % (sequential_cutoff, set_path)
        _exec_shell1(cmd, p=True)

    def set_cache_mode(self, coredev, cache_mode=None):
        list_mode = ['writeback', 'writethrough', 'writearound', 'none']
        if cache_mode is None or len(cache_mode) == 0:
            cache_mode = self.config.cache_mode
        elif cache_mode not in list_mode:
            raise Exp(errno.EINVAL, "bad cache mode:%s, must be writethrough|writeback|writearound|none" % (cache_mode))

        coredevname = coredev.split('/')[2]
        set_path = '/sys/block/%s/bcache/cache_mode' % coredevname
        cmd = 'echo %s > %s' % (cache_mode, set_path)
        _exec_shell1(cmd, p=True)

    def get_relate_coredev_by_cachedev(self, cachedev):
        list_coredev = []
        cset_uuid = self.get_cset_uuid_by_dev(cachedev)
        cachedir = os.path.join('/sys/fs/bcache', cset_uuid)
        files = os.listdir(cachedir)
        key = 'bdev'
        for f in files:
            if key in f:
                abs_ln = os.path.join(cachedir, f)
                target = os.readlink(abs_ln)
                coredev = target.split('/')[-2]
                list_coredev.append(coredev)
        return list_coredev

    def list_coredevs_by_cachedev(self, cachedev):
        list_coredevs = []
        list_coredevname = self.get_relate_coredev_by_cachedev(cachedev)
        for coredevname in list_coredevname:
            coredev = '/dev/' + coredevname
            list_coredevs.append(coredev)

        return list_coredevs

    def is_clean_state(self, coredev):
        coredevname = coredev.split('/')[2]
        _exec_state = 'cat /sys/block/%s/bcache/state' % (coredevname)
        try:
            out, err = _exec_shell1(_exec_state, p=False)
        except Exp as e:
            return False
        state = out.strip()
        if 'clean' in state:
            return True
        else:
            return False

    def __del_coredev(self, coredev, cset_uuid):
        coredevname = coredev.split('/')[2]
        abs_detach = '/sys/block/' + coredevname + '/bcache/detach'
        _exec_detach = 'echo %s > %s' % (cset_uuid, abs_detach)
        _exec_shell1(_exec_detach, p=True)

        abs_stop = '/sys/block/' + coredevname + '/bcache/stop'
        _exec_stop = 'echo 1 > %s' % abs_stop
        _exec_shell1(_exec_stop, p=True)

        _exec_dd = 'dd if=/dev/zero of=%s count=1 bs=1024 seek=4' % coredev
        _exec_shell1(_exec_dd, p=True)

    def is_all_deleted(self, list_coredev):
        index = 0
        length = len(list_coredev)
        if length == 0:
            return True

        def __is_coredev_deleted(dev):
            _exec_check = "bcache-super-show %s 2>/dev/null| grep 'backing device'" % dev
            try:
                out, err = _exec_shell1(_exec_check, p=True)
                return False
            except Exp as e:
                #  deleted success
                return True

        while index < length:
            deleted = __is_coredev_deleted(list_coredev[index])
            if not deleted:
                print 'waiting for delete %s' % (list_coredev[index])
                time.sleep(1)
                continue
            else:
                index += 1
        return True

    def del_cachedev(self, cachedev, list_coredev, is_flush):
        if not self.is_cachedev(cachedev):
            raise Exp(errno.EINVAL, 'not a cachedev\n')

        cset_uuid = self.get_cset_uuid_by_dev(cachedev)
        deleted = self.is_all_deleted(list_coredev)
        if deleted:
            _exec_stop_cache = 'echo 1 > /sys/fs/bcache/%s/stop' % cset_uuid
            _exec_shell1(_exec_stop_cache, p=True)

            _exec_dd = 'dd if=/dev/zero of=%s count=1 bs=1024 seek=4' % cachedev
            _exec_shell1(_exec_dd, p=True)

    def del_coredev(self, coredev, is_flush):
        if not self.is_coredev(coredev):
            raise Exp(errno.EINVAL, '%s not a coredev\n' % (coredev))

        fd = _lock_file1("/var/run/cache_log.lock")
        cmd = "grep 'delcoredev %s' %s || echo 'delcoredev %s %d' >> %s" %\
                  (coredev, self.cache_log, coredev, int(is_flush), self.cache_log)
        _exec_shell1(cmd, p=False)
        _unlock_file1(fd)

        pid = os.fork()
        if pid == 0:
            if is_flush:
                while not self.is_clean_state(coredev):
                    time.sleep(2)
            cset_uuid = self.get_cset_uuid_by_dev(coredev)
            self.__del_coredev(coredev, cset_uuid)

            # delete from /opt/fusionstack/log/cache.log
            fd = _lock_file1("/var/run/cache_log.lock")
            cmd = "sed -i '/delcoredev %s/d' %s" % (coredev.replace("/dev/", "\/dev\/"), self.cache_log)
            _exec_shell1(cmd, p=False)
            _unlock_file1(fd)
            sys.exit(0)
        elif pid > 0:
            return
        else:
            raise Exp(errno.EPERM, 'fork error\n')

    def flush_cachedev(self, cachedev):
        return

    def flush_coredev(self, coredev):
        return

    def is_valid_bcachedev(self, bcachedev):
        if bcachedev.startswith("/dev/"):
            bcachename = bcachedev[5:]
        else:
            bcachename = bcachedev

        if not bcachename.startswith("bcache"):
            return False

        cmd = "lsblk | grep '%s ' | wc -l" % (bcachename)
        out, err = _exec_shell1(cmd, p=False)
        count = int(out.strip('\n'))
        if count == 2: #must be 2 records, the one is in coredev, the other is in cachedev
            return True
        else:
            return False

    def _cacheset(self, key, value, cachedev_name):
        path = "/sys/block/%s/bcache/%s" % (cachedev_name, key)
        if os.path.isfile(path):
            _set_value(path, value)
        else:
            # raise Exp(errno.ENOENT, "key:%s not found in %s." % (key, cachedev_name))
            pass

    def cacheset(self, key, value, cachedev):
        fail_list = []
        succ_list = []

        if cachedev is not None:
            if cachedev.startswith("/dev/"):
                cachedev_name = cachedev[5:]
            else:
                cachedev_name = cachedev

            self._cacheset(key, value, cachedev_name)
        else:
            for cachedev_name in os.listdir("/dev"):
                if cachedev_name.startswith("bcache"):
                    if os.path.isdir('/dev/%s' % cachedev_name):
                        continue

                    try:
                        self._cacheset(key, value, cachedev_name)
                        succ_list.append(cachedev_name)
                    except Exp, e:
                        _derror("%s set %s %s fail. %s" % (cachedev_name, key, value, e.err))
                        fail_list.append(cachedev_name)
                        continue

        if len(fail_list):
            print "cache set finish :"
            print "    fail list : %s" % (fail_list)
            print "    success list : %s" % (succ_list)
        else:
            # print "cache set %s value %s ok!" % (key, value)
            pass

    def _cacheget(self, key, cachedev_name):
        path = "/sys/block/%s/bcache/%s" % (cachedev_name, key)
        if not os.path.isfile(path):
            raise Exp(errno.ENOENT, "key:%s not find in %s." % (key, cachedev_name))

        value = _get_value(path)
        print "%s : %s value --> %s" % (cachedev_name, key, value)

    def cacheget(self, key, cachedev):
        fail_list = []
        succ_list = []

        if cachedev is not None:
            if cachedev.startswith("/dev/"):
                cachedev_name = cachedev[5:]
            else:
                cachedev_name = cachedev

            self._cacheget(key, cachedev_name)
        else:
            for cachedev_name in os.listdir("/dev"):
                if cachedev_name.startswith("bcache"):
                    if os.path.isdir('/dev/%s' % cachedev_name):
                        continue

                    try:
                        self._cacheget(key, cachedev_name)
                        succ_list.append(cachedev_name)
                    except Exp, e:
                        _derror("%s get %s fail. %s" % (cachedev_name, key, e.err))
                        fail_list.append(cachedev_name)
                        continue
